﻿#-------------------------------------------------------------------------------
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
#   (c) 2007 K.Danilov aka koder
#   distributed under GNU GPL v2.0 - see
#   http://www.gnu.org/licenses/gpl.html for full licens text
#   this file is part of plazyn python library
#
#   This program is free software; you can redistribute it and/or
#   modify it under the terms of the GNU General Public License
#   as published by the Free Software Foundation; (using version 2 of licence)
#    
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#   
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#-------------------------------------------------------------------------------
__doc__ = """This module provide lazy calculations for Python.
Module containe three classes for different use cases
(but no one can cover all of them).
Classes are not accessable directly (due to optimization).
Instead you can use decorators or functions.

NSLazy(Not So Lazy) is simplest one ( use nslazy function or nslazy_dec decorator). 
It save calculations result in 'v' attribute, and do all calculations when you 
access to this attribute first time.

>>> def func(x,y):
...     print "executed"
...     return x + y
...
>>> x = nslazy(func,1,2)
>>> x.v
executed
3
>>> x.v
3

Althow after calculations have done method __getattribute__ of class
removed => next attribute get operations don't lead to call python function.
This class althow catch access to __dict__ attribute, so you can write even this

>>> x = nslazy(func,1,2)
>>> x.__dict__['v']
executed
3
>>> x.__dict__['v']
3

Next class MimicLazyValue( mimic_lazy_value and mimiclazy_dec). This is the first 
class that try to fool everyone's around and mimic the calculations result object.
Two diffent techics are used, depend on calculations result object. 
First - for builtin object and instances of classes with __slots__.
In this case they work as proxy and just redirect all call(attribute
access,etc) to underhood object. Second techics, more appropriate, used for 
user-defined objects without __slots__. In this case MimicLazyValue really
became underhood object - it's copy all attributes, including __class__ and
__dict__.

And the last one class is HackLazyValue(use hacklazyvalue function or hlazy_dec).
This class try to replace herself with calculations result object 
in stack frame. When it define access it analise stack frame
find herself in it and write result object in this place. HackLazyValue can do such
thing with global(module,class) frame, function frame(using C-code) and
closure values(through C-code too), but if HackLazyValue don't
stored directly in frame object( m = [hacklazy(func,1,2)]) magic don't work.
If this methods are fail somehow it's fall back to MimicLazyValue method.

>>> @hlazy_dec
... def my_lazy_func(x,y):
...     return x + y
...
>>> c = my_lazy_func(2,1)
>>> type(c)
<class '__main__.HackLazyValue'>
>>> print c
3
>>> type(c)
<type 'int'>
>>>

         !!! Увага,Внимание,Attention,Atención,Attenzione,Achtung !!! 
                             Грабли aka issues.

Big ones:

    All this methods, except NSLazy, are dirty hacks. Sometimes they don't work
    as expected, and even can produce hard-to-find bugs.
    As example - you might want to track all instances of some class, but
    MimicLazyValue can create new instance, which don't tracked.
    Even more - MimicLazyValue create two objects, wich use he same __dict__
    attribute. So, if function store/(pass in some place) reference to
    created object, and than use it this will althow affect to lazy object.
    If underhood object will be deleted, and destructor do somethig like
    close file hanles/cleanup other object/.... it may prevent
    lazy value from normal functioning.

    HackLazyValue wouldn't work with C-code. It's modify only Python stack,
    so if you already have c-pointer to python object you don't see any changes.

    If object defined in c-code it's pretty much possible that it's have
    data fields, which will not be copyed to MimicLazyValue/HackLazyValue
    along with __dict__ and __class__.
    As result working with this object from C-code lead to realy big troubles.

    Althow, due to python limitations, it's impossible to hanlde next cases:
    id(obj),obj is obj1,type(obj),if obj :... Last one is make impossible to do
    lazy calculations with functions which return bool objects (one only exception
    - if you use x == True instead of just x ;) ).
    
Not so big:
    
    Using methods of object class you can easily bypass
    proxy mechanism of any object. Like this:
    obj.__getattr__(some_obj,'name') <- this wouldn't call __getattr__ or
    __getattribute__ of some_obj.__class__.
    
    MimicLazy don't work in case two operand operators with two lazy objects,
    like this:

    @mimiclazy_dec
    def laz():
        return 1
    print laz() + laz()
    
    TypeError: unsupported operand type(s) for +: 'MimicLazyValue'
        and 'MimicLazyValue'.
    (I still don't know how solve this in proper way)
"""
#-------------------------------------------------------------------------------
import sys
import types
try:
    import lazy_help as lazy_help
except ImportError:
    lazy_help = None
#-------------------------------------------------------------------------------
try:
    from functools import update_wrapper as _update_wrapper
    # for wrap method of some classes, like int
    def update_wrapper(x,y):
        try:
            return _update_wrapper(x,y)
        except:
            return x
except ImportError:
    update_wrapper = lambda x,_ : x
#-------------------------------------------------------------------------------
def nslazy_dec(func):
    def closure(*dt,**mp):
        return nslazy(func,*dt,**mp)
    closure = update_wrapper(closure,func)
    return closure
#-------------------------------------------------------------------------------
def mimiclazy_dec(func):
    def closure(*dt,**mp):
        return mimic_lazy_value(func,*dt,**mp)
    closure = update_wrapper(closure,func)
    return closure
#-------------------------------------------------------------------------------
def hlazy_dec(func):
    def closure(*dt,**mp):
        return hack_lazy_value(func,*dt,**mp)
    closure = update_wrapper(closure,func)
    return closure
#-------------------------------------------------------------------------------
def nslazy(func,*dt,**mp):
    class NSLazy(object):
        def __init__(self,func,dt,mp):
            self.fnc = lambda : func(*dt,**mp)
        def __getattribute__(self,name):
            if name in ('v','__dict__'):
                self.v = self.fnc()
                del self.__class__.__getattribute__
                return object.__getattribute__(self,name)
            return object.__getattribute__(self,name)
    return NSLazy(func,dt,mp)
#-------------------------------------------------------------------------------
# python special methods list
# type conversion
_special_methods  = "str,unicode,int,float,long,complex,"
# container methods
_special_methods += "getitem,setitem,delitem,iter,len,contains,"
_special_methods += "getslice,setslice,delslice,"
# numeric methods
# integer/fload operations
_special_methods += "add,iadd,radd,"
_special_methods += "sub,isub,rsub,"
_special_methods += "mul,imul,rmul,"
_special_methods += "div,rdiv,idiv,"
_special_methods += "divmod,rdivmod,"
_special_methods += "floordiv,rfloordiv,ifloordiv,"
_special_methods += "truediv,rtruediv,itruediv,"
_special_methods += "pow,rpow,ipow,"
# sign change methods
_special_methods += "neg,pos,abs,"
# bitwise operations
_special_methods += "invert,"
_special_methods += "lshift,rlshift,ilshift,"
_special_methods += "rshift,rrshift,irshift,"
_special_methods += "and,rand,iand,"
_special_methods += "or,ror,ior,"
_special_methods += "xor,rxor,ixor,"
# comparetive methods and bool methods
#_special_methods += "lt,le,eq,gt,ge,ne,nonzero,"
_special_methods += "cmp,"
# descriptor methods
_special_methods += "delete,get,set"
# attribute access methods
# don't used in this module code, but let it be
# special_methods += "getattr,setattr,delattr,getattribute,"
# lifecycle methods - don't used in this module too
# special_methods += "new,init,del,"
# with statement suppor methods
if sys.version_info >= (2,5):
    _special_methods += "enter,exit,"
# other methods
# iterator method next() don't listened here due to it don't follow
# python special methods naming convention __some-name__
_special_methods += "oct,hex,index,coerce,getnewargs,hash,call"
#-------------------------------------------------------------------------------
special_methods = [ "__%s__" % i for i in _special_methods.split(',')]
special_methods.append('next')
#-------------------------------------------------------------------------------
atom_internal_types = [ types.BooleanType,
                        types.LongType,
                        types.NoneType,
                        types.StringType,
                        types.UnicodeType,
                        types.FloatType,
                        types.IntType]
#-------------------------------------------------------------------------------
other_internal_types = [types.BufferType,
                        types.MemberDescriptorType,
                        types.BuiltinFunctionType,
                        types.MethodType,
                        types.BuiltinMethodType,
                        types.ModuleType,
                        types.ClassType,
                        types.CodeType,
                        types.NotImplementedType,
                        types.ComplexType,
                        types.ObjectType,
                        types.DictProxyType,
                        types.SliceType,
                        types.DictType,
                        types.DictionaryType,
                        types.EllipsisType,
                        types.TracebackType,
                        types.FileType,
                        types.TupleType,
                        types.TypeType,
                        types.FrameType,
                        types.UnboundMethodType,
                        types.FunctionType,
                        types.GeneratorType,
                        types.XRangeType,
                        types.GetSetDescriptorType,
                        types.InstanceType,                              
                        types.LambdaType,
                        types.ListType]
#-------------------------------------------------------------------------------
all_internal_types = dict((i,None) for i in other_internal_types + \
                                            atom_internal_types)
#-------------------------------------------------------------------------------
def _mimic(obj):
    def dumb_proxify(obj,cls,ocls):
        def getattr_proxy(self,name):
            return getattr(obj,name)
        cls.__getattribute__ = getattr_proxy
        def setattr_proxy(self,name,val):
            return setattr(obj,name,val)
        cls.__setattr__ = setattr_proxy
        def delattr_proxy(self,name):
            return delattr(obj,name)
        cls.__delattr__ = delattr_proxy
        for nm in special_methods:
            try:
                fnc = getattr(ocls,nm)
                def clmaker(fnc):
                    def func1(self,*dt,**mp):
                        return fnc(obj,*dt,**mp)
                    return func1
                func1 = clmaker(fnc)
                func1 = update_wrapper(func1,fnc)
                setattr(cls,nm,func1)
            except AttributeError,x:
                assert hasattr(cls,nm)
                delattr(cls,nm)
    ids = id(obj)
    dct = object.__getattribute__(obj,'__dict__')
    lv = dct[ids]
    if lv is None:
        return dct[ids + 1]
    nobj = dct[ids + 1] = lv()
    dct[ids] = None
    cls = object.__getattribute__(obj,'__class__')
    ocls = nobj.__class__
    if type(nobj) in all_internal_types:
        dumb_proxify(nobj,cls,ocls)
    else:
        try:
            object.__setattr__(obj,'__class__',ocls)
            object.__setattr__(obj,'__dict__',nobj.__dict__)
            # FIXME: add checking that attributes __cycle_ref__ & __previos_del__
            #        don't used
            object.__setattr__(obj,'__cycle_ref__',nobj)
            # prevent from deleting nobj beefore obj and calling it's destructor
            # while nobj are still used, althow prevent from calling obj destructor
            if hasattr(ocls,'__del__'):
                if not hasattr(ocls.__del__,'__monkey_patched__'):
                    # TODO: this function can be moved to module top level
                    def mk_new_del(obj):
                        if hasattr(obj,'__cycle_ref__'):
                            # TODO: check is it possible to remove 
                            #       first and third line
                            x = obj.__cycle_ref__
                            del obj.__cycle_ref__
                            del x
                        else:
                            obj.__class__.__previos_del__(obj)
                    mk_new_del.__monkey_patched__ = True
                    ocls.__previos_del__ = ocls.__del__
                    ocls.__del__ = mk_new_del
            nobj = obj
        except TypeError:
            # obj have __slots__ attribute ((((
            # fall back to less appropriate mimick method
            dumb_proxify(obj,cls,ocls)
    return nobj
#-------------------------------------------------------------------------------
def mimic_lazy_value(func,*dt,**mp):
    #---------------------------------------------------------------------------
    class MimicLazyValue(object):
        def __init__(self,func,dt,mp):
            dct = object.__getattribute__(self,'__dict__')
            ids = id(self)
            dct[ids] = lambda : func(*dt,**mp)
        #-----------------------------------------------------------------------
        lcs = {}
        for nm in special_methods:
            def clmaker(nm):
                def func(self,*dt,**mp):
                    obj = _mimic(self)
                    return getattr(obj,nm)(*dt,**mp)
                return func
            lcs[nm] = clmaker(nm)
        locals().update(lcs)
        #-----------------------------------------------------------------------
        def __getattribute__(self,name):
            obj = _mimic(self)
            return getattr(obj,name)
        #-----------------------------------------------------------------------
        def __setattr__(self,name,val):
            obj = _mimic(self)
            return setattr(obj,name,val)
        #-----------------------------------------------------------------------
        def __delattr__(self,name,val):
            obj = _mimic(self)
            return delattr(obj,name,val)
    #---------------------------------------------------------------------------
    return MimicLazyValue(func,dt,mp)
#-------------------------------------------------------------------------------
def _hack_install(obj,deep = 1):
    x = sys._getframe(deep + 1)
    glob_var = False # is var founded in locals?
    #first look up variable in locals
    for name,val in x.f_locals.iteritems():
        if val is obj:
            break
    if val is not obj:
        #try to find variable in globals
        for name,val in x.f_globals.iteritems():
            if val is obj:
                glob_var = True
                break
        if not glob_var:
            raise RuntimeError("Can't install value - object not found")
    #---------------------------------------------------------------------------
    #do calculaions, if need
    ids = id(obj)
    dct = object.__getattribute__(obj,'__dict__')
    lv = dct[ids]
    if lv is None:
        nobj = dct[ids + 1]
    else:
        nobj = dct[ids + 1] = lv()
    dct[ids] = None
    #---------------------------------------------------------------------------
    try:
        #try install directly
        if glob_var:
            x.f_globals[name] = nobj
        else:
            sys._getframe(deep + 1).f_locals[name] = nobj
            if sys._getframe(deep + 1).f_locals[name] is not nobj:
                #we in function frame - install object from python code 
                #is impossible, fallback to C code
                raise NameError("This is dumb exception. Now try to use C-code")
    except:
        if lazy_help is None:
            raise
        lazy_help.SetFrameObject_ByName(sys._getframe(deep + 1),name,nobj)
    x = None
    return nobj
#-------------------------------------------------------------------------------
def hack_lazy_value(func,*dt,**mp):
    class HackLazyValue(object):
        def __init__(self,func,dt,mp):
            dct = object.__getattribute__(self,'__dict__')
            ids = id(self)
            dct[ids] = lambda : func(*dt,**mp)
        #-----------------------------------------------------------------------
        lcs = {}
        for nm in special_methods:
            def clmaker(nm):
                def func(self,*dt,**mp):
                    try:
                        _hack_install(self)
                    except:
                        pass
                    obj = _mimic(self)
                    return getattr(obj,nm)(*dt,**mp)
                return func
            lcs[nm] = clmaker(nm)
        locals().update(lcs)
        #-----------------------------------------------------------------------
        def __getattribute__(self,name):
            try:
                _hack_install(self)
            except:
                pass
            obj = _mimic(self)
            return getattr(obj,name)
        #-----------------------------------------------------------------------
        def __setattr__(self,name,val):
            try:
                _hack_install(self)
            except:
                pass
            obj = _mimic(self)
            return setattr(obj,name,val)
        #-----------------------------------------------------------------------
        def __delattr__(self,name,val):
            try:
                _hack_install(self)
            except:
                pass
            obj = _mimic(self)
            return setattr(obj,name,val)
    #---------------------------------------------------------------------------
    return HackLazyValue(func,dt,mp)
#-------------------------------------------------------------------------------
